# vBulletin Calendar component
# by Cee Pee

import forumapi
from deps.Form import Form
import datetime
import re

__all__ = [
	# Utilities
	"prepare_event",
	
	# Extensible objects
	"Calendar", "Event", "CalendarPager",
]

## Utilities ##

def prepare_event(session, c=None, type="single"):
	"""Prepares to create an event on the given calendar. The `type` specifies
	what kind of event should be created: `"single"` for a single, all-day
	event, `None` for a ranged event, or `"recur"` for a recurring event.
	
	Returns a `PreparedPost` that needs its `.message` and `.title` properties
	set."""
	
	c = Calendar.obj(session, c)
	
	if c is not None:
		identifier = "c", c.id
	else:
		identifier = None
	
	form_url = "calendar.php?do=add&type={0}{1}".format(type, "&{0}={1}".format(*identifier) if identifier else "")
	
	form_page = session.get_page(form_url)
	
	soup = session.soup(form_page)
	
	session.retrieve_metadata(soup=soup, loc="postform", c=c)
	
	form_soup = soup.find("form", action=lambda url: "do=update" in url)
	if not form_soup:
		raise(forumapi.PermissionError(session.get_error(soup) or "Event form not found."))
	
	post_data = Form(form_soup, session=session)
	
	return(session.module.get_attr("PreparedPost")(session, "addevent", identifier, post_data, c=c))

## Hooks ##

def _retrieve_metadata_page(session=None, kw=None, **kwargs):
	if "e" in kw:
		kw["e"] = Event.obj(session, kw["e"])
		return(("calendar.php?e={0}".format(kw["e"].id), "calevent"))
	
	if "c" in kw:
		kw["c"] = Calendar.obj(session, kw["c"])
		return(("calendar.php?c={0}".format(kw["c"].id), "calendar"))

def _retrieve_metadata(session=None, kw=None, soup=None, loc=None, current=None, breadcrumb=None, **kwargs):
	if "e" in kw:
		kw["e"]._title = current
		if len(breadcrumb) > 0 and isinstance(breadcrumb[-1], Calendar):
			kw["e"]._calendar = breadcrumb[-1]
	if "c" in kw and loc == "calendar":
		kw["c"]._name = current

def _get_breadcrumb(session=None, kw=None, breadcrumb=None, link_qs=None, name=None, **kwargs):
	if "c" in link_qs:
		if "c" in kw:
			if kw["c"] is None:
				kw["c"] = Calendar(session, id=int(link_qs["c"][0]), name=name)
			else:
				kw["c"]._name = name
			
			breadcrumb.append(kw["c"])
		else:
			return(("c", Calendar))

def _prepared_post_submit(preparedpost=None, response_qs=None, **kwargs):
	if preparedpost.type == "addevent":
		id = int(response_qs["e"][0])
		
		# TODO: More data
		event = Event(preparedpost.session, id=id, content=self.post_data["message"], title=self.post_data["title"])
		
		if "c" in preparedpost.kw and preparedpost.kw["c"]:
			event._calendar = preparedpost.kw["c"]
			
			#preparedpost.kw["c"].events.events[DATE].append(event)
		
		return(event)

forumapi.hooks.add("vb_retrieve_metadata_page", _retrieve_metadata_page)
forumapi.hooks.add("vb_retrieve_metadata", _retrieve_metadata)
forumapi.hooks.add("vb_get_breadcrumb", _get_breadcrumb)
forumapi.hooks.add("vb_prepared_post_submit", _prepared_post_submit)

## Classes ##

class Calendar(forumapi.ExtensibleObject):
	@classmethod
	def obj(cls, session, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], int):
			return(cls(session, *a[1:], id=a[0], **kw))
		elif isinstance(a[0], str):
			return(cls(session, *a[1:], name=a[0], **kw))
		else:
			return(None)
	
	def __init__(self, session, id=None, name=None, events=None):
		if not(id or name):
			raise(forumapi.ForumAPIException("At least one of id or name required."))
		
		self.session = session
		
		self._id = id
		self._name = name
		
		self.events = CalendarPager(self, events)
		
		self._find_instances()
	
	def _merge(self, other):
		if self._id:
			other._id = self._id
		
		if self._name:
			other._name = self._name
		
		other.events.merge(self.events)
	
	@property
	def data(self):
		return({
			"id": self._id,
			"name": self._name,
			"events": forumapi._flatten_data(self.events),
		})
	
	@classmethod
	def get_calendars(cls, session):
		if hasattr(session, "calendars"):
			return(session.calendars)
		else:
			default = Calendar(session, id=0)
			default.events.get_page(display="month")
			
			return(session.calendars)
	
	@property
	def id(self):
		if self._id is not None:
			return(self._id)
		else:
			self.__class__.get_calendars(self.session)
			return(self._id)
	
	@property
	def name(self):
		if self._name is not None:
			return(self._name)
		else:
			self.events.get_page()
			return(self._name)
	
	def __eq__(self, other):
		if isinstance(other, Calendar):
			return(self.id == other.id)
		elif isinstance(other, int):
			return(self.id == other)
		elif isinstance(other, str):
			return(self.name == other)
		else:
			return(False)
	
	def __int__(self):
		return(self.id)
	
	def __str__(self):
		return(self.name)
	
	def __repr__(self):
		return("<Calendar {self.id} \"{self.name}\">".format(self=self))

class Event(forumapi.ExtensibleObject):
	@classmethod
	def obj(cls, session, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], int):
			return(cls(session, *a[1:], id=a[0], **kw))
		else:
			return(None)
	
	def __init__(self, session, id=None, title=None, content=None, calendar=None, user=None, date=None, start_time=None, end_time=None, recurrence=None):
		if not(id or (calendar and title and date)):
			raise(ValueError("At least one of id or calendar/title/date required."))
		
		self.session = session
		
		self._calendar = Calendar.obj(session, calendar)
		
		self._id = id
		self._title = title
		self._content = content
		
		self._user = self.module.get_attr("User").obj(session, user)
		
		self._date = date
		self._start_time = start_time
		self._end_time = end_time
		self._recurrence = recurrence
		
		self._find_instances()
	
	def _merge(self, other):
		self._merge_obj(other, "_calendar")
		
		if self._id:
			other._id = self._id
		
		if self._title:
			other._title = self._title
		
		if not other._content or (self._content and not isinstance(self._content, BBCode)):
			other._content = self._content
		
		if self._user:
			other._user = self._user
		
		if self._date:
			other._date = self._date
		
		if self._start_time:
			other._start_time = self._start_time
		if self._end_time:
			other._end_time = self._end_time
		
		if self._recurrence:
			other._recurrence = self._recurrence
	
	@property
	def data(self):
		return({
			"id": self._id,
			"title": self._title,
			"content": forumapi._flatten_data(self._content),
			"calendar": int(self.calendar) if self._calendar is not None else None,
			"user": int(self.user) if self._user is not None else None,
			"date": self._date,
			"start_time": self._start_time,
			"end_time": self._end_time,
			"recurrence": self._recurrence,
		})
	
	@property
	def calendar(self):
		if self._calendar:
			return(self._calendar)
		else:
			self.get_page()
			return(self._calendar)
	
	@property
	def id(self):
		if self._id:
			return(self._id)
		else:
			self.get_page()
			return(self._id)
	
	@property
	def title(self):
		if self._title:
			return(self._title)
		else:
			self.get_page()
			return(self._title)
	
	@property
	def content(self):
		if self._content:
			return(self._content)
		else:
			self.get_page()
			return(self._content)
	
	@property
	def user(self):
		if self._user:
			return(self._user)
		else:
			self.get_page()
			return(self._user)
	
	@property
	def date(self):
		if self._date:
			return(self._date)
		else:
			#self.get_page()
			return(self._date)
	
	@property
	def start_time(self):
		if self._start_time:
			return(self._start_time)
		else:
			#self.get_page()
			return(self._start_time)
	
	@property
	def end_time(self):
		if self._end_time:
			return(self._end_time)
		else:
			#self.get_page()
			return(self._end_time)
	
	@property
	def recurrence(self):
		if self._recurrence:
			return(self._recurrence)
		else:
			#self.get_page()
			return(self._recurrence)
	
	def get_page(self):
		"""Retrieves the page on which this event can be found, and sets data
		from the page for this event and any other events on the page."""
		
		if not self._id:
			self.calendar.events.get_page(year=self.date.year, month=self.date.month, day=self.date.day)
		else:
			page = self.session.get_page("calendar.php?e={0}".format(self.id))
			soup = self.session.soup(page)
			
			self.session.retrieve_metadata(soup=soup, loc="calevent", e=self)
			
			# Calendar should now be set...
			self.calendar.events.retrieve_day_events(soup, date=self._date)
	
	def __eq__(self, other):
		if isinstance(other, Event):
			if self._id and other._id:
				return(self.id == other.id)
			elif self._calendar and other._calendar and self._date and other._date and self._title and other._title:
				return(self.calendar == other.calendar and self.date == other.date and self.title == other.title)
			else:
				return(False)
		elif isinstance(other, int):
			return(self.id == other)
		elif isinstance(other, str):
			return(self.title == other)
		else:
			return(False)
	
	def __int__(self):
		return(self.id)
	
	def __str__(self):
		return(self.title)
	
	def __repr__(self):
		return("<Event {0}\"{self.title}\"{1}{2}>".format("{self.id} ".format(self=self) if self._id else "", " on {date}".format(date=str(self.date)) if self._date else "", " in c{self.calendar.id}".format(self=self) if self._calendar else "", self=self))

class CalendarPager(forumapi.Mergeable, forumapi.Data):
	def __init__(self, calendar, events=None):
		self.calendar = calendar
		self.session = calendar.session
		
		self.events = {}
		if isinstance(events, dict):
			self._merge_dict(events)
		
		self.length = 0
	
	def get_page(self, **kw):
		kw["c"] = self.calendar.id
		
		date = {}
		display = None
		loc = "calendar"
		if "week" in kw:
			display = "week"
		elif "year" in kw:
			date["year"] = int(kw["year"])
			if "month" in kw:
				date["month"] = int(kw["month"])
				if "day" in kw:
					date["day"] = int(kw["day"])
					display = "day"
					kw["day"] = str(kw["year"]) + "-" + str(kw["month"]) + "-" + str(kw["day"])
					del kw["year"]
					del kw["month"]
				else:
					display = "month"
			else:
				display = "year"
		else:
			# Default view...
			display = "month"
			
			# Assuming that our cookies aren't screwed up... (TODO)
			today = datetime.date.today()
			date["year"] = today.year
			date["month"] = today.month
		
		if display == "day":
			kw["do"] = "getinfo"
			loc = "calday"
		else:
			kw["do"] = "display" + display
		
		response = self.session.get_page("calendar.php", post_data=kw, method="get")
		soup = self.session.soup(response)
		
		self.retrieve_data(soup=soup, date=date, display=display, loc=loc)
	
	def retrieve_data(self, soup=None, date=None, display=None, loc=None):
		"""Retrieves data from a given `BeautifulSoup` object of a calendar
		page."""
		
		self.session.retrieve_metadata(soup=soup, loc=loc, c=self.calendar)
		
		if display == "month":
			table = soup.findAll("div", "page")[1].find("table", "tborder")
			
			for row in table.findAll("tr")[1:]:
				for cell in row.findAll("td", "alt2"):
					events_soup = cell.findAll("div", "smallfont")
					
					day_span = cell.find("div", "thead").find("span")
					if not day_span.string:
						day_span = day_span.find("a")
					
					day = int(str(day_span.string).strip())
					
					for event in events_soup:
						if event.find("em"):
							day_date = datetime.date(date["year"], date["month"], day)
							events = []
							for event_title in event.find("a")["title"].split("\n"):
								events.append(Event(self.session, calendar=self.calendar, title=event_title.strip(), date=day_date))
							
							self.events[str(day_date)] = events
							self.length += len(events)
						else:
							self.retrieve_event(event.find("a"), day=day, **date)
		elif display == "week":
			table = soup.findAll("div", "page")[1].find("table", "tborder")
			
			for cell in table.findAll("td", "alt1"):
				link = cell.parent.previousSibling.previousSibling.find("a")
				day_date = datetime.date(*forumapi.qs_dict_from_url(event_link["href"])["day"][0].split("-"))
				
				events_soup = cell.findAll("div", recursive=False)

				for event in events_soup:
					blocks = event.findAll("div")
					if blocks:
						# TODO: Retrieve start/end time
						self.retrieve_event(blocks[-1].find("a"), date=day_date)
					else:
						# Birthdays
						for link in event.findAll("a"):
							self.retrieve_event(link, date=day_date)
		elif display == "year":
			# TODO: Add objects with event title? We could of course explode
			# our dictionaries by storing dates that don't have any events...
			pass
		elif display == "day":
			self.retrieve_day_events(soup, date=datetime.date(date["year"], date["month"], date["day"]))
		
		# Calendar jump
		if not hasattr(self.session, "calendars"):
			self.session.calendars = []
			
			caljump = soup.find("select", {"name": "c"})
			for cal_option in caljump.findAll("option"):
				attrs = dict(cal_option.attrs)
				id = int(attrs["value"])
				name = str(cal_option.string)
				if "selected" in attrs:
					self.calendar._id = id
					self.calendar._name = name
				
				self.session.calendars.append(Calendar(self.session, id=id, name=name))
	
	_onclick_pattern = None
	
	@property
	def onclick_pattern(self):
		if self._onclick_pattern:
			return(self._onclick_pattern)
		else:
			self.__class__._onclick_pattern = re.compile(r"^window.*day=(?P<year>[0-9]+)-(?P<month>[0-9]+)-(?P<day>[0-9]+)")
			return(self._onclick_pattern)
	
	def retrieve_event(self, event_link, date):
		if "member.php" in event_link["href"]:
			user_id = int(forumapi.qs_dict_from_url(event_link["href"])["u"][0])
			
			age = str(event_link.nextSibling.string).strip()
			if age:
				birth_date = datetime.date(int(date.year) - int(self.age_pattern.match(age).group(1)), date.month, date.day)
			else:
				birth_date = datetime.date(1900, date.month, date.day)
			
			u = User(self.session, id=user_id, name=str(event_link.string))
		else:
			event_id = int(forumapi.qs_dict_from_url(event_link["href"])["e"][0])
			title = str(event_link.string)
			if title.endswith(" (...)"):
				title = None
			
			content = str(event_link["title"])
			if content.endswith("..."):
				content = None
			
			date_str = str(date)
			if date_str not in self.events:
				self.events[date_str] = []
			
			self.events[date_str].append(Event(self.session, calendar=self.calendar, id=event_id, title=title, content=content, date=date))
			self.length += 1
	
	_age_pattern = None
	
	@property
	def age_pattern(self):
		if self._age_pattern:
			return(self._age_pattern)
		else:
			self.__class__._age_pattern = re.compile(r"\((\d+)\)")
	
	def retrieve_day_events(self, soup, date=None):
		"""Retrieves data from a calendar page for a single day, or an event
		page. A list of `Event` objects found on the page is added to
		`self.events`."""
		
		events = []
		
		for page in soup.findAll("div", "page")[1:-1]:
			event_title = str(page.find("td", "tcat").string)
			
			calendar_name = str(page.find("td", "thead").contents[-1].string)[2:].strip()
			
			event_data = page.find("td", "alt1")
			
			event_user_link = event_data.find("a", "bigusername")
			event_user_id = int(forumapi.qs_dict_from_url(event_user_link["href"])["u"][0])
			
			event_user = forumapi.User(self.session, id=event_user_id)
			event_user.retrieve_name(event_user_link)
			
			event_times = event_data.find("div")
			
			event_link = event_times.find("a")
			event_id = int(forumapi.qs_dict_from_url(event_link["href"])["e"][0])
			
			# TODO: Retrieve start/end time, etc.
			
			event_body = page.find("td", "alt2")
			event_content = self.module.get_attr("BBCode")(self.session, event_body.find("div"))
			
			events.append(Event(self.session, id=event_id, calendar=self.calendar, title=event_title, date=date, user=event_user, content=event_content))
		
		self.events[str(date)] = events
		self.length += len(events)
	
	def merge(self, other):
		"""Merges a `CalendarPager` or a dictionary with dates as keys into this
		object, making sure that all usable data is retained."""
		
		if isinstance(other, CalendarPager):
			self._merge_dict(other.events)
		elif isinstance(other, dict):
			self._merge_dict(other)
	
	def _merge_dict(self, data):
		for date, events in data.items():
			if date in self.events:
				for event in events:
					if event in self.events[date]:
						continue
					
					self.events[date].append(Event.obj(self.session, event))
					self.length += 1
			else:
				self.events[date] = [Event.obj(self.session, event) for event in events]
				self.length += len(event)
	
	@property
	def data(self):
		return(forumapi._flatten_data(self.events))
	
	def __contains__(self, event):
		event = Event.obj(self.session, event)
		
		return(event.calendar == self.calendar)
	
	def __len__(self):
		return(self.length)
	
	def __iter__(self):
		return(CalendarIterator(self))
	
	def __reversed__(self):
		return(ReversedCalendarIterator(self))
	
	def __repr__(self):
		return("CalendarPager({events})".format(events=repr(self.events)))

class CalendarIterator(object):
	def __init__(self, pager):
		self.pager = pager
		self.date = None
		self.item = -1
	
	def __iter__(self):
		return(self)
	
	def __next__(self):
		if self.date is None:
			self.date = datetime.date(*[int(d) for d in sorted(self.pager.events.keys())[0].split("-")])
		
		if self.date not in self.pager.events:
			self.pager.get_page(year=self.date.year, month=self.date.month, day=self.date.day)
		
		try:
			self.item += 1
			return(self.pager.events[self.date][self.item])
		except:
			self.item = -1
			self.date += datetime.timedelta(days=1)

class ReversedCalendarIterator(CalendarIterator):
	def __init__(self, pager):
		self.pager = pager
		self.date = None
		self.item = 0
	
	def __iter__(self):
		return(self)
	
	def __next__(self):
		if self.date is None:
			self.date = datetime.date(*[int(d) for d in sorted(self.pager.events.keys())[-1].split("-")])
		
		if self.date not in self.pager.events:
			self.pager.get_page(year=self.date.year, month=self.date.month, day=self.date.day)
		
		try:
			self.item -= 1
			return(self.pager.events[self.date][self.item])
		except:
			self.item = 0
			self.date -= datetime.timedelta(days=1)
